<?php
declare (strict_types = 1);

namespace app;

use think\App;
use think\exception\ValidateException;
use think\middleware\AllowCrossDomain;
use think\Validate;

/**
 * 控制器基础类
 */
abstract class BaseController
{
    /**
     * Request实例
     * @var \think\Request
     */
    protected $request;

    /**
     * 应用实例
     * @var \think\App
     */
    protected $app;

    /**
     * 是否批量验证
     * @var bool
     */
    protected $batchValidate = false;

    /**
     * 控制器中间件
     * @var array
     */
    protected $middleware = [
        /** 允许跨域 */
        // AllowCrossDomain::class
    ];

    /**
     * 构造方法
     * @access public
     * @param  App  $app  应用对象
     */
    public function __construct(App $app)
    {
        $this->app     = $app;
        $this->request = $this->app->request;

        // 控制器初始化
        $this->initialize();
    }

    // 初始化
    protected function initialize()
    {}

    /**
     * 验证数据
     * @access protected
     * @param  array        $data     数据
     * @param  string|array $validate 验证器名或者验证规则数组
     * @param  array        $message  提示信息
     * @param  bool         $batch    是否批量验证
     * @return array|string|true
     * @throws ValidateException
     */
    protected function validate(array $data, $validate, array $message = [], bool $batch = false)
    {
        if (is_array($validate)) {
            $v = new Validate();
            $v->rule($validate);
        } else {
            if (strpos($validate, '.')) {
                // 支持场景
                [$validate, $scene] = explode('.', $validate);
            }
            $class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
            $v     = new $class();
            if (!empty($scene)) {
                $v->scene($scene);
            }
        }

        $v->message($message);

        // 是否批量验证
        if ($batch || $this->batchValidate) {
            $v->batch(true);
        }

        return $v->failException(true)->check($data);
    }

    /**
     * 反射获取自身注解参数
     * [注意] 在后台新增编辑接口使用本方法时，参数id=0会影响新增数据的情况。因为当$params中包含id=0，然后调用$item->save($params)后
     * 得到的 $item 里的id并不是数据库自增的，而是依然保持0，这会导致之后对$item调用任何保存等操作都失去更新条件导致失败
     * @param string $methodName 要传入的方法名，直接使用__METHOD__即可，强烈建议传入，否则性能开销较大
     * @param array $expects 要排除的字段
     * @example $params = $this->getDocParams(__METHOD__, ['id']);
     * @return array
     */
    protected function getDocParams($methodName = null, $expects = [])
    {
        // 使用 debug_backtrace 会增加 70% 的性能开销，强烈建议使用__METHOD__传入方法名
        if ($methodName === null) {
            $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
            $caller = $backtrace[1] ?? null;
            if (!$caller) return [];
            $methodName = $caller['class'] . '::' . $caller['function'];
        }

        list($class, $method) = explode('::', $methodName);
        $methodRef = (new \ReflectionClass($class))->getMethod($method);
        $comment = $methodRef->getDocComment();

        $line_array = preg_split('/\r\n|\r|\n/', $comment);
        $results = [];
        foreach ($line_array as $line) {
            // 过滤无@开头的项
            if (strpos($line, '@') === false) continue;
            // 移除@之前的符号 [/ * @title 标题] ===> [title 标题]
            $res = preg_replace("/.+@/", '', $line);
            // 除去多个空格的情况 [param int id  商品ID] ===> [param int id 商品ID]
            $res = preg_replace("/\s+/", ' ', $res);
            // 按空格分隔 ['param', 'int', 'id', '商品ID'], 
            $res = explode(' ', $res);
            if ($res[0] != 'param')  continue;

            $paramName = str_replace('$', '', $res[2] ?? '');
            if (!$paramName) continue;
            if (in_array($paramName, $expects)) continue;

            $results[] = $paramName;
        }
        return request()->only($results);
    }

}
